diff --git a/assets/src/bundles/webapp/webapp.css b/assets/src/bundles/webapp/webapp.css
index 2a6f7793..06954f92 100644
--- a/assets/src/bundles/webapp/webapp.css
+++ b/assets/src/bundles/webapp/webapp.css
@@ -1,705 +1,703 @@
 /**
  * Copyright (C) 2018-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 html {
     height: 100%;
     overflow-x: hidden;
     scroll-behavior: auto !important;
 }
 
 body {
     min-height: 100%;
     margin: 0;
     position: relative;
     padding-bottom: 120px;
 }
 
 a:active,
 a.active {
     outline: none;
 }
 
 code {
     background-color: #f9f2f4;
 }
 
 pre code {
     background-color: transparent;
 }
 
 footer {
     background-color: #262626;
     color: #fff;
     font-size: 0.8rem;
     position: absolute;
     bottom: 0;
     width: 100%;
-    padding-top: 20px;
-    padding-bottom: 20px;
+    padding-top: 10px;
+    padding-bottom: 10px;
 }
 
 footer a,
 footer a:visited,
 footer a:hover {
     color: #fecd1b;
 }
 
 footer a:hover {
     text-decoration: underline;
 }
 
 .link-color {
     color: #fecd1b;
 }
 
 pre {
     background-color: #f5f5f5;
     border: 1px solid #ccc;
     border-radius: 4px;
     padding: 9.5px;
     font-size: 0.8rem;
 }
 
 .btn.active {
     background-color: #e7e7e7;
 }
 
 .card {
     margin-bottom: 5px !important;
     overflow-x: auto;
 }
 
 .navbar-brand {
     padding: 5px;
     margin-right: 0;
 }
 
 .table {
     margin-bottom: 0;
 }
 
 .swh-table thead {
     background-color: #f2f4f5;
     border-top: 1px solid rgba(0, 0, 0, 0.2);
     font-weight: normal;
 }
 
 .swh-table-striped th {
     border-top: none;
 }
 
 .swh-table-striped tbody tr:nth-child(even) {
     background-color: #f2f4f5;
 }
 
 .swh-table-striped tbody tr:nth-child(odd) {
     background-color: #fff;
 }
 
 .swh-web-app-link a {
     text-decoration: none;
     border: none;
 }
 
 .swh-web-app-link:hover {
     background-color: #efeff2;
 }
 
 .table > thead > tr > th {
     border-top: none;
     border-bottom: 1px solid #e20026;
 }
 
 .table > tbody > tr > td {
     border-style: none;
 }
 
 .sitename .first-word,
 .sitename .second-word {
     color: rgba(0, 0, 0, 0.75);
     font-weight: normal;
     font-size: 1.2rem;
 }
 
 .sitename .first-word {
     font-family: 'Alegreya Sans', sans-serif;
 }
 
 .sitename .second-word {
     font-family: 'Alegreya', serif;
 }
 
 .swh-counter {
     font-size: 150%;
 }
 
 @media (max-width: 600px) {
     .swh-counter-container {
         margin-top: 1rem;
     }
 }
 
 .swh-http-error {
     margin: 0 auto;
     text-align: center;
 }
 
 .swh-http-error-head {
     color: #2d353c;
     font-size: 30px;
 }
 
 .swh-http-error-code {
     bottom: 60%;
     color: #2d353c;
     font-size: 96px;
     line-height: 80px;
     margin-bottom: 10px !important;
 }
 
 .swh-http-error-desc {
     font-size: 12px;
     color: #647788;
     text-align: center;
 }
 
 .swh-http-error-desc pre {
     display: inline-block;
     text-align: left;
     max-width: 800px;
     white-space: pre-wrap;
 }
 
 .swh-list-unstyled {
     list-style: none;
 }
 
 .popover {
     max-width: 97%;
     z-index: 40000;
 }
 
 .modal {
     text-align: center;
     padding: 0 !important;
     z-index: 50000;
 }
 
 .modal::before {
     content: '';
     display: inline-block;
     height: 100%;
     vertical-align: middle;
     margin-right: -4px;
 }
 
 .modal-dialog {
     display: inline-block;
     text-align: left;
     vertical-align: middle;
 }
 
 .dropdown-submenu {
     position: relative;
 }
 
 .dropdown-submenu .dropdown-menu {
     top: 0;
     left: -100%;
     margin-top: -5px;
     margin-left: -2px;
 }
 
 .dropdown-item:hover,
 .dropdown-item:focus {
     background-color: rgba(0, 0, 0, 0.1);
 }
 
 a.dropdown-left::before {
     content: "\f035e";
     font-family: 'Material Design Icons';
     display: block;
     width: 20px;
     height: 20px;
     float: left;
     margin-left: 0;
 }
 
 #swh-navbar {
     border-top-style: none;
     border-left-style: none;
     border-right-style: none;
     border-bottom-style: solid;
     border-bottom-width: 5px;
     border-image: linear-gradient(to right, rgb(226, 0, 38) 0%, rgb(254, 205, 27) 100%) 1 1 1 1;
     width: 100%;
     padding: 5px;
     margin-bottom: 10px;
     margin-top: 30px;
     justify-content: normal;
     flex-wrap: nowrap;
     height: 72px;
     overflow: hidden;
 }
 
 #back-to-top {
     display: none;
     position: fixed;
     bottom: 30px;
     right: 30px;
     z-index: 10;
 }
 
 #back-to-top a img {
     display: block;
     width: 32px;
     height: 32px;
     background-size: 32px 32px;
     text-indent: -999px;
     overflow: hidden;
 }
 
 .swh-top-bar {
     direction: ltr;
     height: 30px;
     position: fixed;
     top: 0;
     left: 0;
     width: 100%;
     z-index: 99999;
     background-color: #262626;
     color: #fff;
     text-align: center;
     font-size: 14px;
 }
 
 .swh-top-bar ul {
     margin-top: 4px;
     padding-left: 0;
     white-space: nowrap;
 }
 
 .swh-top-bar li {
     display: inline-block;
     margin-left: 10px;
     margin-right: 10px;
 }
 
 .swh-top-bar a,
 .swh-top-bar a:visited {
     color: white;
 }
 
 .swh-top-bar a.swh-current-site,
 .swh-top-bar a.swh-current-site:visited {
     color: #fecd1b;
 }
 
 .swh-position-left {
     position: absolute;
     left: 0;
 }
 
 .swh-position-right {
     position: absolute;
     right: 0;
 }
 
 .swh-background-gray {
     background: #efeff2;
 }
 
 .swh-donate-link {
     border: 1px solid #fecd1b;
     background-color: #e20026;
     color: white !important;
     padding: 3px;
     border-radius: 3px;
 }
 
 .swh-navbar-content h4 {
     padding-top: 7px;
 }
 
 .swh-navbar-content .bread-crumbs {
     display: block;
     margin-left: -40px;
 }
 
 .swh-navbar-content .bread-crumbs li.bc-no-root {
     padding-top: 7px;
 }
 
 .main-sidebar {
     margin-top: 30px;
 }
 
 .content-wrapper {
     background: none;
 }
 
 .brand-image {
     max-height: 40px;
 }
 
 .brand-link {
     padding-top: 18.5px;
     padding-bottom: 18px;
     padding-left: 4px;
     border-bottom: 5px solid #e20026 !important;
 }
 
 .navbar-header a,
 ul.dropdown-menu a,
 ul.navbar-nav a,
 ul.nav-sidebar a {
     border-bottom-style: none;
     color: #323232;
 }
 
 .swh-sidebar .nav-link.active {
     color: #323232 !important;
     background-color: #e7e7e7 !important;
 }
 
 .nav-tabs .nav-link.active {
     border-top: 3px solid #e20026;
 }
 
 .swh-image-error {
     width: 80px;
     height: auto;
 }
 
 @media (max-width: 600px) {
     .card {
         min-width: 80%;
     }
 
     .swh-image-error {
         width: 40px;
         height: auto;
     }
 
     .swh-donate-link {
         display: none;
     }
 }
 
 .form-check-label {
     padding-top: 4px;
 }
 
 .swhid {
     white-space: pre-wrap;
 }
 
 .swhid .swhid-option {
     display: inline-block;
     margin-right: 5px;
     line-height: 1rem;
 }
 
 .nav-pills .nav-link:not(.active):hover {
     color: rgba(0, 0, 0, 0.55);
 }
 
 .swh-heading-color {
     color: #e20026 !important;
 }
 
 .sidebar-mini.sidebar-collapse .main-sidebar:hover {
     width: 4.6rem;
 }
 
 .sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel > .info,
 .sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-sidebar .nav-link p,
 .sidebar-mini.sidebar-collapse .main-sidebar:hover .brand-text {
     visibility: hidden !important;
 }
 
 .sidebar .nav-link p,
 .main-sidebar .brand-text,
 .sidebar .user-panel .info {
     transition: none;
 }
 
 .sidebar-mini.sidebar-mini.sidebar-collapse .sidebar {
     padding-right: 0;
 }
 
 .swh-words-logo {
     position: absolute;
     top: 0;
     left: 0;
     width: 73px;
     height: 73px;
     text-align: center;
     font-size: 10pt;
     color: rgba(0, 0, 0, 0.75);
 }
 
 .swh-words-logo:hover {
     text-decoration: none;
 }
 
 .swh-words-logo-swh {
     line-height: 1;
     padding-top: 13px;
     visibility: hidden;
 }
 
 hr.swh-faded-line {
     border: 0;
     height: 1px;
     background-image: linear-gradient(to left, #f0f0f0, #8c8b8b, #f0f0f0);
 }
 
 /* Ensure that section title with link is colored like standard section title */
 .swh-readme h1 a,
 .swh-readme h2 a,
 .swh-readme h3 a,
 .swh-readme h4 a,
 .swh-readme h5 a,
 .swh-readme h6 a {
     color: #e20026;
 }
 
 /* Make list compact in reStructuredText rendering */
 .swh-rst li p {
     margin-bottom: 0;
 }
 
 .swh-readme-txt pre {
     background: none;
     border: none;
 }
 
 .swh-coverage-col {
     padding-left: 10px;
     padding-right: 10px;
 }
 
 .swh-coverage {
     height: calc(65px + 1em);
     padding-top: 0.3rem;
     border: none;
 }
 
 .swh-coverage a {
     text-decoration: none;
 }
 
 .swh-coverage-logo {
     display: block;
     width: 100%;
     height: 50px;
     margin-left: auto;
     margin-right: auto;
     object-fit: contain;
 
     /* polyfill for old browsers, see https://github.com/bfred-it/object-fit-images */
     font-family: 'object-fit: contain;';
 }
 
 .swh-coverage-list {
     width: 100%;
     height: 320px;
     border: none;
 }
 
 tr.swh-tr-hover-highlight:hover td {
     background: #ededed;
 }
 
 tr.swh-api-doc-route a {
     text-decoration: none;
 }
 
 .swh-apidoc .col {
     margin: 10px;
 }
 
 .swh-apidoc .swh-rst blockquote {
     border: 0;
     margin: 0;
     padding: 0;
 }
 
 a.toggle-col {
     text-decoration: none;
 }
 
 a.toggle-col.col-hidden {
     text-decoration: line-through;
 }
 
 .admonition.warning {
     background: #fcf8e3;
     border: 1px solid #faebcc;
     padding: 15px;
     border-radius: 4px;
 }
 
 .admonition.warning p {
     margin-bottom: 0;
 }
 
 .admonition.warning .first {
     font-size: 1.5rem;
 }
 
 .swh-popover {
     max-height: 50vh;
     overflow-y: auto;
     overflow-x: auto;
     padding: 0;
 }
 
 @media screen and (min-width: 768px) {
     .swh-popover {
         max-width: 50vw;
     }
 }
 
 .swh-popover pre {
     white-space: pre-wrap;
     margin-bottom: 0;
 }
 
 .d3-wrapper {
     position: relative;
     height: 0;
     width: 100%;
     padding: 0;
 
     /* padding-bottom will be overwritten by JavaScript later */
     padding-bottom: 100%;
 }
 
 .d3-wrapper > svg {
     position: absolute;
     height: 100%;
     width: 100%;
     left: 0;
     top: 0;
 }
 
 div.d3-tooltip {
     position: absolute;
     text-align: center;
     width: auto;
     height: auto;
     padding: 2px;
     font: 12px sans-serif;
     background: white;
     border: 1px solid black;
     border-radius: 4px;
     pointer-events: none;
 }
 
 .page-link {
     cursor: pointer;
 }
 
 .wrapper {
     overflow: hidden;
 }
 
 .swh-badge {
     padding-bottom: 1rem;
     cursor: pointer;
 }
 
 .swh-badge-html,
 .swh-badge-md,
 .swh-badge-rst {
     white-space: pre-wrap;
 }
 
 /* Material Design icons alignment tweaks */
 
 .mdi {
     display: inline-block;
 }
 
 .mdi-camera {
     transform: translateY(1px);
 }
 
 .mdi-source-commit {
     transform: translateY(2px);
 }
 
 /* To set icons at a fixed width. Great to use when different
    icon widths throw off alignment. Courtesy of Font Awesome. */
 .mdi-fw {
     text-align: center;
     width: 1.25em;
 }
 
 .main-header .nav-link {
     height: inherit;
 }
 
 .nav-sidebar .nav-header:not(:first-of-type) {
     padding-top: 1rem;
 }
 
 .nav-sidebar .nav-link {
     padding-top: 0;
     padding-bottom: 0;
 }
 
 .nav-sidebar > .nav-item .nav-icon {
     vertical-align: sub;
 }
 
 .swh-search-icon {
     line-height: 1rem;
     vertical-align: middle;
 }
 
 .swh-search-navbar {
     position: absolute;
     top: 0.7rem;
     right: 15rem;
     z-index: 50000;
     width: 500px;
 }
 
 .sidebar-collapse .swh-search-navbar {
     right: 4rem;
 }
 
 .swh-corner-ribbon {
     width: 200px;
     background: #fecd1b;
     color: #e20026;
     position: absolute;
     text-align: center;
-    line-height: 50px;
     letter-spacing: 1px;
     box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
     top: 55px;
     right: -50px;
     left: auto;
     transform: rotate(45deg);
     z-index: 2000;
 }
 
 @media screen and (max-width: 600px) {
     .swh-corner-ribbon {
-        line-height: 30px;
         top: 53px;
         right: -65px;
     }
 }
 
 .invalid-feedback {
     font-size: 100%;
 }
diff --git a/cypress/integration/origin-search.spec.js b/cypress/integration/origin-search.spec.js
index 48903c4c..d6858e61 100644
--- a/cypress/integration/origin-search.spec.js
+++ b/cypress/integration/origin-search.spec.js
@@ -1,556 +1,556 @@
 /**
  * Copyright (C) 2019-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 const nonExistentText = 'NoMatchExists';
 
 let origin;
 let url;
 
 function doSearch(searchText, searchInputElt = '#swh-origins-url-patterns') {
   if (searchText.startsWith('swh:')) {
     cy.intercept('**/api/1/resolve/**')
       .as('swhidResolve');
   }
   cy.get(searchInputElt)
     // to avoid sending too much SWHID validation requests
     // as cypress insert character one by one when using type
     .invoke('val', searchText.slice(0, -1))
     .type(searchText.slice(-1))
     .get('.swh-search-icon')
-    .click();
+    .click({force: true});
   if (searchText.startsWith('swh:')) {
     cy.wait('@swhidResolve');
   }
 }
 
 function searchShouldRedirect(searchText, redirectUrl) {
   doSearch(searchText);
   cy.location('pathname')
     .should('equal', redirectUrl);
 }
 
 function searchShouldShowNotFound(searchText, msg) {
   doSearch(searchText);
   if (searchText.startsWith('swh:')) {
     cy.get('.invalid-feedback')
       .should('be.visible')
       .and('contain', msg);
   }
 }
 
 function stubOriginVisitLatestRequests(status = 200, response = {type: 'tar'}, aliasSuffix = '') {
   cy.intercept({url: '**/visit/latest/**'}, {
     body: response,
     statusCode: status
   }).as(`originVisitLatest${aliasSuffix}`);
 }
 
 describe('Test origin-search', function() {
   before(function() {
     origin = this.origin[0];
     url = this.Urls.browse_search();
   });
 
   beforeEach(function() {
     cy.visit(url);
   });
 
   it('should have focus on search form after page load', function() {
     cy.get('#swh-origins-url-patterns')
       .should('have.attr', 'autofocus');
     // for some reason, autofocus is not honored when running cypress tests
     // while it is in non controlled browsers
     // .should('have.focus');
   });
 
   it('should show in result when url is searched', function() {
     cy.get('#swh-origins-url-patterns')
       .type(origin.url);
     cy.get('.swh-search-icon')
       .click();
 
     cy.get('#origin-search-results')
       .should('be.visible');
     cy.contains('tr', origin.url)
       .should('be.visible')
       .find('.swh-visit-status')
       .find('i')
       .should('have.class', 'mdi-check-bold')
       .and('have.attr', 'title',
            'Software origin has been archived by Software Heritage');
 
     const browseOriginUrl = `${this.Urls.browse_origin()}?origin_url=${encodeURIComponent(origin.url)}`;
     cy.get('tr a')
       .should('have.attr', 'href', browseOriginUrl);
   });
 
   it('should remove origin URL with no archived content', function() {
     stubOriginVisitLatestRequests(404);
 
     cy.get('#swh-origins-url-patterns')
       .type(origin.url);
     cy.get('.swh-search-icon')
       .click();
 
     cy.wait('@originVisitLatest');
 
     cy.get('#origin-search-results')
       .should('be.visible')
       .find('tbody tr').should('have.length', 0);
 
     stubOriginVisitLatestRequests(200, {}, '2');
 
     cy.get('.swh-search-icon')
       .click();
 
     cy.wait('@originVisitLatest2');
 
     cy.get('#origin-search-results')
       .should('be.visible')
       .find('tbody tr').should('have.length', 0);
 
   });
 
   it('should filter origins by visit type', function() {
     cy.intercept('**/visit/latest/**').as('checkOriginVisits');
     cy.get('#swh-origins-url-patterns')
       .type('http');
 
     for (let visitType of ['git', 'tar']) {
       cy.get('#swh-search-visit-type')
         .select(visitType);
 
       cy.get('.swh-search-icon')
         .click();
 
       cy.wait('@checkOriginVisits');
 
       cy.get('#origin-search-results')
         .should('be.visible');
 
       cy.get('tbody tr td.swh-origin-visit-type').then(elts => {
         for (let elt of elts) {
           cy.get(elt).should('have.text', visitType);
         }
       });
     }
   });
 
   it('should show not found message when no repo matches', function() {
     searchShouldShowNotFound(nonExistentText,
                              'No origins matching the search criteria were found.');
   });
 
   it('should add appropriate URL parameters', function() {
     // Check all three checkboxes and check if
     // correct url params are added
     cy.get('#swh-search-origins-with-visit')
       .check({force: true})
       .get('#swh-filter-empty-visits')
       .check({force: true})
       .get('#swh-search-origin-metadata')
       .check({force: true})
       .then(() => {
         const searchText = origin.url;
         doSearch(searchText);
         cy.location('search').then(locationSearch => {
           const urlParams = new URLSearchParams(locationSearch);
           const query = urlParams.get('q');
           const withVisit = urlParams.has('with_visit');
           const withContent = urlParams.has('with_content');
           const searchMetadata = urlParams.has('search_metadata');
 
           assert.strictEqual(query, searchText);
           assert.strictEqual(withVisit, true);
           assert.strictEqual(withContent, true);
           assert.strictEqual(searchMetadata, true);
         });
       });
   });
 
   it('should search in origin intrinsic metadata', function() {
     cy.intercept('GET', '**/origin/metadata-search/**').as(
       'originMetadataSearch'
     );
     cy.get('#swh-search-origins-with-visit')
       .check({force: true})
       .get('#swh-filter-empty-visits')
       .check({force: true})
       .get('#swh-search-origin-metadata')
       .check({force: true})
       .then(() => {
         const searchText = 'plugin';
         doSearch(searchText);
         console.log(searchText);
         cy.wait('@originMetadataSearch').then((req) => {
           expect(req.response.body[0].metadata.metadata.description).to.equal(
             'Line numbering plugin for Highlight.js'
             // metadata is defined in _TEST_ORIGINS variable in swh/web/tests/data.py
           );
         });
       });
   });
 
   it('should not send request to the resolve endpoint', function() {
     cy.intercept(`${this.Urls.api_1_resolve_swhid('').slice(0, -1)}**`)
       .as('resolveSWHID');
 
     cy.intercept(`${this.Urls.api_1_origin_search(origin.url)}**`)
       .as('searchOrigin');
 
     cy.get('#swh-origins-url-patterns')
       .type(origin.url);
 
     cy.get('.swh-search-icon')
       .click();
 
     cy.wait('@searchOrigin');
 
     cy.xhrShouldBeCalled('resolveSWHID', 0);
     cy.xhrShouldBeCalled('searchOrigin', 1);
   });
 
   context('Test pagination', function() {
     it('should not paginate if there are not many results', function() {
       // Setup search
       cy.get('#swh-search-origins-with-visit')
         .uncheck({force: true})
         .get('#swh-filter-empty-visits')
         .uncheck({force: true})
         .then(() => {
           const searchText = 'libtess';
 
           // Get first page of results
           doSearch(searchText);
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 1);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://github.com/memononen/libtess2');
 
           cy.get('#origins-prev-results-button')
             .should('have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('have.class', 'disabled');
         });
     });
 
     it('should paginate forward when there are many results', function() {
       stubOriginVisitLatestRequests();
       // Setup search
       cy.get('#swh-search-origins-with-visit')
         .uncheck({force: true})
         .get('#swh-filter-empty-visits')
         .uncheck({force: true})
         .then(() => {
           const searchText = 'many.origins';
 
           // Get first page of results
           doSearch(searchText);
           cy.wait('@originVisitLatest');
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 100);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://many.origins/1');
           cy.get('.swh-search-result-entry#origin-99 td a')
             .should('have.text', 'https://many.origins/100');
 
           cy.get('#origins-prev-results-button')
             .should('have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get second page of results
           cy.get('#origins-next-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 100);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://many.origins/101');
           cy.get('.swh-search-result-entry#origin-99 td a')
             .should('have.text', 'https://many.origins/200');
 
           cy.get('#origins-prev-results-button')
             .should('not.have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get third (and last) page of results
           cy.get('#origins-next-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 50);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://many.origins/201');
           cy.get('.swh-search-result-entry#origin-49 td a')
             .should('have.text', 'https://many.origins/250');
 
           cy.get('#origins-prev-results-button')
             .should('not.have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('have.class', 'disabled');
         });
     });
 
     it('should paginate backward from a middle page', function() {
       stubOriginVisitLatestRequests();
       // Setup search
       cy.get('#swh-search-origins-with-visit')
         .uncheck({force: true})
         .get('#swh-filter-empty-visits')
         .uncheck({force: true})
         .then(() => {
           const searchText = 'many.origins';
 
           // Get first page of results
           doSearch(searchText);
           cy.wait('@originVisitLatest');
 
           cy.get('#origins-prev-results-button')
             .should('have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get second page of results
           cy.get('#origins-next-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('#origins-prev-results-button')
             .should('not.have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get first page of results again
           cy.get('#origins-prev-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 100);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://many.origins/1');
           cy.get('.swh-search-result-entry#origin-99 td a')
             .should('have.text', 'https://many.origins/100');
 
           cy.get('#origins-prev-results-button')
             .should('have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
         });
     });
 
     it('should paginate backward from the last page', function() {
       stubOriginVisitLatestRequests();
       // Setup search
       cy.get('#swh-search-origins-with-visit')
         .uncheck({force: true})
         .get('#swh-filter-empty-visits')
         .uncheck({force: true})
         .then(() => {
           const searchText = 'many.origins';
 
           // Get first page of results
           doSearch(searchText);
           cy.wait('@originVisitLatest');
 
           cy.get('#origins-prev-results-button')
             .should('have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get second page of results
           cy.get('#origins-next-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('#origins-prev-results-button')
             .should('not.have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get third (and last) page of results
           cy.get('#origins-next-results-button a')
             .click();
 
           cy.get('#origins-prev-results-button')
             .should('not.have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('have.class', 'disabled');
 
           // Get second page of results again
           cy.get('#origins-prev-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 100);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://many.origins/101');
           cy.get('.swh-search-result-entry#origin-99 td a')
             .should('have.text', 'https://many.origins/200');
 
           cy.get('#origins-prev-results-button')
             .should('not.have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get first page of results again
           cy.get('#origins-prev-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 100);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://many.origins/1');
           cy.get('.swh-search-result-entry#origin-99 td a')
             .should('have.text', 'https://many.origins/100');
 
           cy.get('#origins-prev-results-button')
             .should('have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
         });
     });
   });
 
   context('Test valid SWHIDs', function() {
     it('should resolve directory', function() {
       const redirectUrl = this.Urls.browse_directory(origin.content[0].directory);
       const swhid = `swh:1:dir:${origin.content[0].directory}`;
 
       searchShouldRedirect(swhid, redirectUrl);
     });
 
     it('should resolve revision', function() {
       const redirectUrl = this.Urls.browse_revision(origin.revisions[0]);
       const swhid = `swh:1:rev:${origin.revisions[0]}`;
 
       searchShouldRedirect(swhid, redirectUrl);
     });
 
     it('should resolve snapshot', function() {
       const redirectUrl = this.Urls.browse_snapshot_directory(origin.snapshot);
       const swhid = `swh:1:snp:${origin.snapshot}`;
 
       searchShouldRedirect(swhid, redirectUrl);
     });
 
     it('should resolve content', function() {
       const redirectUrl = this.Urls.browse_content(`sha1_git:${origin.content[0].sha1git}`);
       const swhid = `swh:1:cnt:${origin.content[0].sha1git}`;
 
       searchShouldRedirect(swhid, redirectUrl);
     });
 
     it('should not send request to the search endpoint', function() {
       const swhid = `swh:1:rev:${origin.revisions[0]}`;
 
       cy.intercept(this.Urls.api_1_resolve_swhid(swhid))
         .as('resolveSWHID');
 
       cy.intercept(`${this.Urls.api_1_origin_search('').slice(0, -1)}**`)
         .as('searchOrigin');
 
       cy.get('#swh-origins-url-patterns')
         .type(swhid);
 
       cy.get('.swh-search-icon')
         .click();
 
       cy.wait('@resolveSWHID');
 
       cy.xhrShouldBeCalled('resolveSWHID', 1);
       cy.xhrShouldBeCalled('searchOrigin', 0);
     });
   });
 
   context('Test invalid SWHIDs', function() {
     it('should show not found for directory', function() {
       const swhid = `swh:1:dir:${this.unarchivedRepo.rootDirectory}`;
       const msg = `Directory with sha1_git ${this.unarchivedRepo.rootDirectory} not found`;
 
       searchShouldShowNotFound(swhid, msg);
     });
 
     it('should show not found for snapshot', function() {
       const swhid = `swh:1:snp:${this.unarchivedRepo.snapshot}`;
       const msg = `Snapshot with id ${this.unarchivedRepo.snapshot} not found!`;
 
       searchShouldShowNotFound(swhid, msg);
     });
 
     it('should show not found for revision', function() {
       const swhid = `swh:1:rev:${this.unarchivedRepo.revision}`;
       const msg = `Revision with sha1_git ${this.unarchivedRepo.revision} not found.`;
 
       searchShouldShowNotFound(swhid, msg);
     });
 
     it('should show not found for content', function() {
       const swhid = `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git}`;
       const msg = `Content with sha1_git checksum equals to ${this.unarchivedRepo.content[0].sha1git} not found!`;
 
       searchShouldShowNotFound(swhid, msg);
     });
 
     function checkInvalidSWHIDReport(url, searchInputElt, swhidInput, validationMessagePattern = '') {
       cy.visit(url);
       doSearch(swhidInput, searchInputElt);
       cy.get(searchInputElt)
         .then($el => $el[0].checkValidity()).should('be.false');
       cy.get(searchInputElt)
         .invoke('prop', 'validationMessage')
         .should('not.equal', '')
         .should('contain', validationMessagePattern);
     }
 
     it('should report invalid SWHID in search page input', function() {
       const swhidInput =
         `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git};lines=45-60/`;
       checkInvalidSWHIDReport(this.Urls.browse_search(), '#swh-origins-url-patterns', swhidInput);
       cy.get('.invalid-feedback')
         .should('be.visible');
     });
 
     it('should report invalid SWHID in top right search input', function() {
       const swhidInput =
         `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git};lines=45-60/`;
       checkInvalidSWHIDReport(this.Urls.browse_help(), '#swh-origins-search-top-input', swhidInput);
     });
 
     it('should report SWHID with uppercase chars in search page input', function() {
       const swhidInput =
         `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git}`.toUpperCase();
       checkInvalidSWHIDReport(this.Urls.browse_search(), '#swh-origins-url-patterns', swhidInput, swhidInput.toLowerCase());
       cy.get('.invalid-feedback')
         .should('be.visible');
     });
 
     it('should report SWHID with uppercase chars in top right search input', function() {
       let swhidInput =
         `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git}`.toUpperCase();
       swhidInput += ';lines=45-60/';
       checkInvalidSWHIDReport(this.Urls.browse_help(), '#swh-origins-search-top-input', swhidInput.toLowerCase());
     });
 
   });
 
 });
diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py
index dae3cf4b..1115f970 100644
--- a/swh/web/common/utils.py
+++ b/swh/web/common/utils.py
@@ -1,351 +1,354 @@
 # Copyright (C) 2017-2021  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 from datetime import datetime, timezone
 import os
 import re
 from typing import Any, Dict, Optional
 
 from bs4 import BeautifulSoup
 from docutils.core import publish_parts
 import docutils.parsers.rst
 import docutils.utils
 from docutils.writers.html5_polyglot import HTMLTranslator, Writer
 from iso8601 import ParseError, parse_date
+from pkg_resources import get_distribution
 from prometheus_client.registry import CollectorRegistry
 
 from django.http import HttpRequest, QueryDict
 from django.urls import reverse as django_reverse
 
 from swh.web.common.exc import BadInputExc
 from swh.web.common.typing import QueryParameters
 from swh.web.config import ORIGIN_VISIT_TYPES, get_config
 
 SWH_WEB_METRICS_REGISTRY = CollectorRegistry(auto_describe=True)
 
 swh_object_icons = {
     "alias": "mdi mdi-star",
     "branch": "mdi mdi-source-branch",
     "branches": "mdi mdi-source-branch",
     "content": "mdi mdi-file-document",
     "directory": "mdi mdi-folder",
     "origin": "mdi mdi-source-repository",
     "person": "mdi mdi-account",
     "revisions history": "mdi mdi-history",
     "release": "mdi mdi-tag",
     "releases": "mdi mdi-tag",
     "revision": "mdi mdi-rotate-90 mdi-source-commit",
     "snapshot": "mdi mdi-camera",
     "visits": "mdi mdi-calendar-month",
 }
 
 
 def reverse(
     viewname: str,
     url_args: Optional[Dict[str, Any]] = None,
     query_params: Optional[QueryParameters] = None,
     current_app: Optional[str] = None,
     urlconf: Optional[str] = None,
     request: Optional[HttpRequest] = None,
 ) -> str:
     """An override of django reverse function supporting query parameters.
 
     Args:
         viewname: the name of the django view from which to compute a url
         url_args: dictionary of url arguments indexed by their names
         query_params: dictionary of query parameters to append to the
             reversed url
         current_app: the name of the django app tighten to the view
         urlconf: url configuration module
         request: build an absolute URI if provided
 
     Returns:
         str: the url of the requested view with processed arguments and
         query parameters
     """
 
     if url_args:
         url_args = {k: v for k, v in url_args.items() if v is not None}
 
     url = django_reverse(
         viewname, urlconf=urlconf, kwargs=url_args, current_app=current_app
     )
 
     if query_params:
         query_params = {k: v for k, v in query_params.items() if v is not None}
 
     if query_params and len(query_params) > 0:
         query_dict = QueryDict("", mutable=True)
         for k in sorted(query_params.keys()):
             query_dict[k] = query_params[k]
         url += "?" + query_dict.urlencode(safe="/;:")
 
     if request is not None:
         url = request.build_absolute_uri(url)
 
     return url
 
 
 def datetime_to_utc(date):
     """Returns datetime in UTC without timezone info
 
     Args:
         date (datetime.datetime): input datetime with timezone info
 
     Returns:
         datetime.datetime: datetime in UTC without timezone info
     """
     if date.tzinfo and date.tzinfo != timezone.utc:
         return date.astimezone(tz=timezone.utc)
     else:
         return date
 
 
 def parse_iso8601_date_to_utc(iso_date: str) -> datetime:
     """Given an ISO 8601 datetime string, parse the result as UTC datetime.
 
     Returns:
         a timezone-aware datetime representing the parsed date
 
     Raises:
         swh.web.common.exc.BadInputExc: provided date does not respect ISO 8601 format
 
     Samples:
         - 2016-01-12
         - 2016-01-12T09:19:12+0100
         - 2007-01-14T20:34:22Z
 
     """
     try:
         date = parse_date(iso_date)
         return datetime_to_utc(date)
     except ParseError as e:
         raise BadInputExc(e)
 
 
 def shorten_path(path):
     """Shorten the given path: for each hash present, only return the first
     8 characters followed by an ellipsis"""
 
     sha256_re = r"([0-9a-f]{8})[0-9a-z]{56}"
     sha1_re = r"([0-9a-f]{8})[0-9a-f]{32}"
 
     ret = re.sub(sha256_re, r"\1...", path)
     return re.sub(sha1_re, r"\1...", ret)
 
 
 def format_utc_iso_date(iso_date, fmt="%d %B %Y, %H:%M UTC"):
     """Turns a string representation of an ISO 8601 datetime string
     to UTC and format it into a more human readable one.
 
     For instance, from the following input
     string: '2017-05-04T13:27:13+02:00' the following one
     is returned: '04 May 2017, 11:27 UTC'.
     Custom format string may also be provided
     as parameter
 
     Args:
         iso_date (str): a string representation of an ISO 8601 date
         fmt (str): optional date formatting string
 
     Returns:
         str: a formatted string representation of the input iso date
     """
     if not iso_date:
         return iso_date
     date = parse_iso8601_date_to_utc(iso_date)
     return date.strftime(fmt)
 
 
 def gen_path_info(path):
     """Function to generate path data navigation for use
     with a breadcrumb in the swh web ui.
 
     For instance, from a path /folder1/folder2/folder3,
     it returns the following list::
 
         [{'name': 'folder1', 'path': 'folder1'},
          {'name': 'folder2', 'path': 'folder1/folder2'},
          {'name': 'folder3', 'path': 'folder1/folder2/folder3'}]
 
     Args:
         path: a filesystem path
 
     Returns:
         list: a list of path data for navigation as illustrated above.
 
     """
     path_info = []
     if path:
         sub_paths = path.strip("/").split("/")
         path_from_root = ""
         for p in sub_paths:
             path_from_root += "/" + p
             path_info.append({"name": p, "path": path_from_root.strip("/")})
     return path_info
 
 
 def parse_rst(text, report_level=2):
     """
     Parse a reStructuredText string with docutils.
 
     Args:
         text (str): string with reStructuredText markups in it
         report_level (int): level of docutils report messages to print
             (1 info 2 warning 3 error 4 severe 5 none)
 
     Returns:
         docutils.nodes.document: a parsed docutils document
     """
     parser = docutils.parsers.rst.Parser()
     components = (docutils.parsers.rst.Parser,)
     settings = docutils.frontend.OptionParser(
         components=components
     ).get_default_values()
     settings.report_level = report_level
     document = docutils.utils.new_document("rst-doc", settings=settings)
     parser.parse(text, document)
     return document
 
 
 def get_client_ip(request):
     """
     Return the client IP address from an incoming HTTP request.
 
     Args:
         request (django.http.HttpRequest): the incoming HTTP request
 
     Returns:
         str: The client IP address
     """
     x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
     if x_forwarded_for:
         ip = x_forwarded_for.split(",")[0]
     else:
         ip = request.META.get("REMOTE_ADDR")
     return ip
 
 
 browsers_supported_image_mimes = set(
     [
         "image/gif",
         "image/png",
         "image/jpeg",
         "image/bmp",
         "image/webp",
         "image/svg",
         "image/svg+xml",
     ]
 )
 
 
 def context_processor(request):
     """
     Django context processor used to inject variables
     in all swh-web templates.
     """
     config = get_config()
     if (
         hasattr(request, "user")
         and request.user.is_authenticated
         and not hasattr(request.user, "backend")
     ):
         # To avoid django.template.base.VariableDoesNotExist errors
         # when rendering templates when standard Django user is logged in.
         request.user.backend = "django.contrib.auth.backends.ModelBackend"
     site_base_url = request.build_absolute_uri("/")
     return {
         "swh_object_icons": swh_object_icons,
         "available_languages": None,
         "swh_client_config": config["client_config"],
         "oidc_enabled": bool(config["keycloak"]["server_url"]),
         "browsers_supported_image_mimes": browsers_supported_image_mimes,
         "keycloak": config["keycloak"],
         "site_base_url": site_base_url,
         "DJANGO_SETTINGS_MODULE": os.environ["DJANGO_SETTINGS_MODULE"],
         "status": config["status"],
+        "swh_web_dev": "localhost" in site_base_url,
         "swh_web_staging": any(
             [
                 server_name in site_base_url
                 for server_name in config["staging_server_names"]
             ]
         ),
+        "swh_web_version": get_distribution("swh.web").version,
         "visit_types": ORIGIN_VISIT_TYPES,
     }
 
 
 def resolve_branch_alias(
     snapshot: Dict[str, Any], branch: Optional[Dict[str, Any]]
 ) -> Optional[Dict[str, Any]]:
     """
     Resolve branch alias in snapshot content.
 
     Args:
         snapshot: a full snapshot content
         branch: a branch alias contained in the snapshot
     Returns:
         The real snapshot branch that got aliased.
     """
     while branch and branch["target_type"] == "alias":
         if branch["target"] in snapshot["branches"]:
             branch = snapshot["branches"][branch["target"]]
         else:
             from swh.web.common import archive
 
             snp = archive.lookup_snapshot(
                 snapshot["id"], branches_from=branch["target"], branches_count=1
             )
             if snp and branch["target"] in snp["branches"]:
                 branch = snp["branches"][branch["target"]]
             else:
                 branch = None
     return branch
 
 
 class _NoHeaderHTMLTranslator(HTMLTranslator):
     """
     Docutils translator subclass to customize the generation of HTML
     from reST-formatted docstrings
     """
 
     def __init__(self, document):
         super().__init__(document)
         self.body_prefix = []
         self.body_suffix = []
 
 
 _HTML_WRITER = Writer()
 _HTML_WRITER.translator_class = _NoHeaderHTMLTranslator
 
 
 def rst_to_html(rst: str) -> str:
     """
     Convert reStructuredText document into HTML.
 
     Args:
         rst: A string containing a reStructuredText document
 
     Returns:
         Body content of the produced HTML conversion.
 
     """
     settings = {
         "initial_header_level": 2,
     }
     pp = publish_parts(rst, writer=_HTML_WRITER, settings_overrides=settings)
     return f'<div class="swh-rst">{pp["html_body"]}</div>'
 
 
 def prettify_html(html: str) -> str:
     """
     Prettify an HTML document.
 
     Args:
         html: Input HTML document
 
     Returns:
         The prettified HTML document
     """
     return BeautifulSoup(html, "lxml").prettify()
diff --git a/swh/web/templates/layout.html b/swh/web/templates/layout.html
index e97007a2..0a27f1f5 100644
--- a/swh/web/templates/layout.html
+++ b/swh/web/templates/layout.html
@@ -1,278 +1,281 @@
 {% comment %}
 Copyright (C) 2015-2021  The Software Heritage developers
 See the AUTHORS file at the top-level directory of this distribution
 License: GNU Affero General Public License version 3, or any later version
 See top-level LICENSE file for more information
 {% endcomment %}
 
 <!DOCTYPE html>
 
 {% load js_reverse %}
 {% load static %}
 {% load render_bundle from webpack_loader %}
 {% load swh_templatetags %}
 
 <html lang="en">
   <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     <title>{% block title %}{% endblock %}</title>
 
     {% render_bundle 'vendors' %}
     {% render_bundle 'webapp' %}
 
     <script>
 /*
 @licstart  The following is the entire license notice for the JavaScript code in this page.
 
 Copyright (C) 2015-2021  The Software Heritage developers
 
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
 published by the Free Software Foundation, either version 3 of the
 License, or (at your option) any later version.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU Affero General Public License for more details.
 
 You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 @licend  The above is the entire license notice for the JavaScript code in this page.
 */
     </script>
 
     <script>
       SWH_CONFIG = {{swh_client_config|jsonify}};
       swh.webapp.sentryInit(SWH_CONFIG.sentry_dsn);
     </script>
 
     <script src="{% url 'js_reverse' %}" type="text/javascript"></script>
 
     <script>
       swh.webapp.setSwhObjectIcons({{ swh_object_icons|jsonify }});
     </script>
 
     {{ request.user.is_authenticated|json_script:"swh_user_logged_in" }}
 
     {% block header %}{% endblock %}
 
     <link rel="icon" href="{% static 'img/icons/swh-logo-32x32.png' %}" sizes="32x32" />
     <link rel="icon" href="{% static 'img/icons/swh-logo-archive-192x192.png' %}" sizes="192x192" />
     <link rel="apple-touch-icon-precomposed" href="{% static 'img/icons/swh-logo-archive-180x180.png' %}" />
     <link rel="search" type="application/opensearchdescription+xml" title="Software Heritage archive of public source code" href="{% static 'xml/swh-opensearch.xml' %}">
     <meta name="msapplication-TileImage" content="{% static 'img/icons/swh-logo-archive-270x270.png' %}" />
 
     {% if "production" in DJANGO_SETTINGS_MODULE  %}
 
       <!-- Matomo -->
       <script type="text/javascript">
         var _paq = window._paq = window._paq || [];
         _paq.push(['trackPageView']);
         (function() {
           var u="https://piwik.inria.fr/";
           _paq.push(['setTrackerUrl', u+'matomo.php']);
           _paq.push(['setSiteId', '59']);
           var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
           g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
         })();
       </script>
       <!-- End Matomo Code -->
 
     {% endif %}
 
   </head>
 
   <body class="hold-transition layout-fixed sidebar-mini">
     <a id="top"></a>
     <div class="wrapper">
       <div class="swh-top-bar">
         <ul>
           <li class="swh-position-left">
             <div id="swh-full-width-switch-container" class="custom-control custom-switch d-none d-lg-block d-xl-block">
               <input type="checkbox" class="custom-control-input" id="swh-full-width-switch" onclick="swh.webapp.fullWidthToggled(event)">
               <label class="custom-control-label font-weight-normal" for="swh-full-width-switch">Full width</label>
             </div>
           </li>
           <li>
             <a href="https://www.softwareheritage.org">Home</a>
           </li>
           <li>
             <a href="https://forge.softwareheritage.org/">Development</a>
           </li>
           <li>
             <a href="https://docs.softwareheritage.org/devel/">Documentation</a>
           </li>
           <li>
             <a class="swh-donate-link" href="https://www.softwareheritage.org/donate">Donate</a>
           </li>
           <li class="swh-position-right">
             <a href="{{ status.server_url }}" target="_blank"
               class="swh-current-status mr-3 d-none d-lg-inline-block d-xl-inline-block">
               <span id="swh-current-status-description">Operational</span>
               <i class="swh-current-status-indicator green"></i>
             </a>
             {% url 'logout' as logout_url %}
             {% if user.is_authenticated %}
               Logged in as
               {% if 'OIDC' in user.backend %}
                 <a href="{% url 'oidc-profile' %}"><strong>{{ user.username }}</strong></a>,
                 <a href="{% url 'oidc-logout' %}?next_path={% url 'logout' %}?remote_user=1">logout</a>
               {% else %}
                 <strong>{{ user.username }}</strong>,
                 <a href="{{ logout_url }}">logout</a>
               {% endif %}
             {% elif oidc_enabled %}
               {% if request.path != logout_url %}
                 <a href="{% url 'oidc-login' %}?next_path={{ request.build_absolute_uri }}">login</a>
               {% else %}
                 <a href="{% url 'oidc-login' %}">login</a>
               {% endif %}
             {% else %}
               {% if request.path != logout_url %}
                 <a href="{% url 'login' %}?next={{ request.build_absolute_uri }}">login</a>
               {% else %}
                 <a href="{% url 'login' %}">login</a>
               {% endif %}
             {% endif %}
           </li>
         </ul>
       </div>
       <nav class="main-header navbar navbar-expand-lg navbar-light navbar-static-top" id="swh-navbar">
         <div class="navbar-header">
           <a class="nav-link swh-push-menu" data-widget="pushmenu" data-enable-remember="true" href="#">
             <i class="mdi mdi-24px mdi-menu mdi-fw" aria-hidden="true"></i>
           </a>
         </div>
         <div class="navbar" style="width: 94%;">
           <div class="swh-navbar-content">
             {% block navbar-content %}{% endblock %}
             {% if request.resolver_match.url_name != 'swh-web-homepage' and request.resolver_match.url_name != 'browse-search' %}
               <form class="form-horizontal d-none d-md-flex input-group swh-search-navbar needs-validation"
                   id="swh-origins-search-top">
                 <input class="form-control"
                   placeholder="Enter a SWHID to resolve or keyword(s) to search for in origin URLs"
                   type="text" id="swh-origins-search-top-input"
                   oninput="swh.webapp.validateSWHIDInput(this)" required/>
                 <div class="input-group-append">
                   <button class="btn btn-primary" type="submit">
                   <i class="swh-search-icon mdi mdi-24px mdi-magnify" aria-hidden="true"></i>
                   </button>
                 </div>
               </form>
             {% endif %}
           </div>
         </div>
       </nav>
     </div>
 
     <aside class="swh-sidebar main-sidebar sidebar-no-expand sidebar-light-primary elevation-4">
       <a href="{% url 'swh-web-homepage' %}" class="brand-link">
         <img class="brand-image" src="{% static 'img/swh-logo.png' %}">
         <div class="brand-text sitename" href="{% url 'swh-web-homepage' %}">
           <span class="first-word">Software</span> <span class="second-word">Heritage</span>
         </div>
       </a>
 
       <a href="/" class="swh-words-logo">
         <div class="swh-words-logo-swh">
           <span class="first-word">Software</span>
           <span class="second-word">Heritage</span>
         </div>
         <span>Archive</span>
       </a>
 
       <div class="sidebar">
         <nav class="mt-2">
           <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
             <li class="nav-header">Features</li>
             <li class="nav-item swh-search-item" title="Search archived software">
               <a href="{% url 'browse-search' %}" class="nav-link swh-search-link">
                 <i style="color: #e20026;" class="nav-icon mdi mdi-24px mdi-magnify"></i>
                 <p>Search</p>
               </a>
             </li>
             <li class="nav-item swh-vault-item" title="Download archived software from the Vault">
               <a href="{% url 'browse-vault' %}" class="nav-link swh-vault-link">
                 <i style="color: #e20026;" class="nav-icon mdi mdi-24px mdi-download"></i>
                 <p>Downloads</p>
               </a>
             </li>
             <li class="nav-item swh-origin-save-item" title="Request the saving of a software origin into the archive">
               <a href="{% url 'origin-save' %}" class="nav-link swh-origin-save-link">
                 <i style="color: #e20026;" class="nav-icon mdi mdi-24px mdi-camera"></i>
                 <p>Save code now</p>
               </a>
             </li>
             <li class="nav-item swh-help-item" title="How to browse the archive ?">
               <a href="{% url 'browse-help' %}" class="nav-link swh-help-link">
                 <i style="color: #e20026;" class="nav-icon mdi mdi-24px mdi-help-circle"></i>
                 <p>Help</p>
               </a>
             </li>
             {% if user.is_authenticated  and user.is_staff %}
               <li class="nav-header">Administration</li>
               <li class="nav-item swh-origin-save-admin-item" title="Save code now administration">
                 <a href="{% url 'admin-origin-save' %}" class="nav-link swh-origin-save-admin-link">
                   <i style="color: #fecd1b;" class="nav-icon mdi mdi-24px mdi-camera"></i>
                   <p>Save code now</p>
                 </a>
               </li>
               <li class="nav-item swh-deposit-admin-item" title="Deposit administration">
                 <a href="{% url 'admin-deposit' %}" class="nav-link swh-deposit-admin-link">
                   <i style="color: #fecd1b;" class="nav-icon mdi mdi-24px mdi-folder-upload"></i>
                   <p>Deposit</p>
                 </a>
               </li>
             {% endif %}
           </ul>
         </nav>
       </div>
     </aside>
 
     <div class="content-wrapper">
       <section class="content">
         <div class="container" id="swh-web-content">
           {% if swh_web_staging %}
-            <div class="swh-corner-ribbon">Staging</div>
+            <div class="swh-corner-ribbon">Staging<br/>v{{ swh_web_version }}</div>
+          {% elif swh_web_dev %}
+            <div class="swh-corner-ribbon">Development<br/>v{{ swh_web_version|split:"+"|first }}</div>
           {% endif %}
           {% block content %}{% endblock %}
         </div>
       </section>
     </div>
 
     {% include "includes/global-modals.html" %}
 
     <footer class="footer">
       <div class="container text-center">
         <a href="https://www.softwareheritage.org">Software Heritage</a> &mdash;
         Copyright (C) 2015&ndash;{% now "Y" %}, The Software Heritage developers.
         License: <a href="https://www.gnu.org/licenses/agpl.html">GNU
-        AGPLv3+</a>. <br /> The source code of Software Heritage <em>itself</em>
+        AGPLv3+</a>. <br/> The source code of Software Heritage <em>itself</em>
         is available on
         our <a href="https://forge.softwareheritage.org/">development
-        forge</a>. <br /> The source code files <em>archived</em> by Software
-        Heritage are available under their own copyright and licenses. <br />
+        forge</a>. <br/> The source code files <em>archived</em> by Software
+        Heritage are available under their own copyright and licenses. <br/>
         <span class="link-color">Terms of use: </span>
         <a href="https://www.softwareheritage.org/legal/bulk-access-terms-of-use/">Archive access</a>,
         <a href="https://www.softwareheritage.org/legal/api-terms-of-use/">API</a>-
         <a href="https://www.softwareheritage.org/contact/">Contact</a>-
         <a href="{% url 'jslicenses' %}" rel="jslicense">JavaScript license information</a>-
-        <a href="{% url 'api-1-homepage' %}">Web API</a>
+        <a href="{% url 'api-1-homepage' %}">Web API</a><br/>
+        swh-web v{{ swh_web_version }}
       </div>
     </footer>
     <div id="back-to-top">
       <a href="#top"><img alt="back to top" src="{% static 'img/arrow-up-small.png' %}" /></a>
     </div>
     <script>
       swh.webapp.setContainerFullWidth();
       var statusServerURL = {{ status.server_url|jsonify }};
       var statusJsonPath = {{ status.json_path|jsonify }};
       swh.webapp.initStatusWidget(statusServerURL + statusJsonPath);
     </script>
   </body>
 
 </html>
diff --git a/swh/web/tests/test_templates.py b/swh/web/tests/test_templates.py
index 44986bf1..13178871 100644
--- a/swh/web/tests/test_templates.py
+++ b/swh/web/tests/test_templates.py
@@ -1,43 +1,63 @@
 # Copyright (C) 2021  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 from copy import deepcopy
 import random
 
+from pkg_resources import get_distribution
+
 from swh.web.common.utils import reverse
 from swh.web.config import STAGING_SERVER_NAMES, get_config
 from swh.web.tests.django_asserts import assert_contains, assert_not_contains
 from swh.web.tests.utils import check_http_get_response
 
+swh_web_version = get_distribution("swh.web").version
+
 
-def test_layout_without_staging_ribbon(client):
+def test_layout_without_ribbon(client):
     url = reverse("swh-web-homepage")
     resp = check_http_get_response(client, url, status_code=200)
     assert_not_contains(resp, "swh-corner-ribbon")
 
 
 def test_layout_with_staging_ribbon(client):
     url = reverse("swh-web-homepage")
     resp = check_http_get_response(
         client, url, status_code=200, server_name=random.choice(STAGING_SERVER_NAMES),
     )
     assert_contains(resp, "swh-corner-ribbon")
+    assert_contains(resp, f"Staging<br/>v{swh_web_version}")
+
+
+def test_layout_with_development_ribbon(client):
+    url = reverse("swh-web-homepage")
+    resp = check_http_get_response(
+        client, url, status_code=200, server_name="localhost",
+    )
+    assert_contains(resp, "swh-corner-ribbon")
+    assert_contains(resp, f"Development<br/>v{swh_web_version.split('+')[0]}")
 
 
 def test_layout_with_oidc_auth_enabled(client):
     url = reverse("swh-web-homepage")
     resp = check_http_get_response(client, url, status_code=200)
     assert_contains(resp, reverse("oidc-login"))
 
 
 def test_layout_without_oidc_auth_enabled(client, mocker):
     config = deepcopy(get_config())
     config["keycloak"]["server_url"] = ""
     mock_get_config = mocker.patch("swh.web.common.utils.get_config")
     mock_get_config.return_value = config
 
     url = reverse("swh-web-homepage")
     resp = check_http_get_response(client, url, status_code=200)
     assert_contains(resp, reverse("login"))
+
+
+def test_layout_swh_web_version_number_display(client):
+    url = reverse("swh-web-homepage")
+    resp = check_http_get_response(client, url, status_code=200)
+    assert_contains(resp, f"swh-web v{swh_web_version}")